리스너 실행 컨텍스트와 교착 상태(Deadlock) 방지 가이드

리스너 실행 컨텍스트와 교착 상태(Deadlock) 방지 가이드

데이터 분산 서비스(DDS)를 활용한 시스템 아키텍처에서 리스너(Listener)는 데이터 수신과 시스템 상태 변화를 비동기적(Asynchronous)으로 감지하는 가장 직관적이고 강력한 메커니즘을 제공한다. 그러나 이러한 편의성 이면에는 멀티스레드 환경에서 발생할 수 있는 가장 치명적이고 진단하기 어려운 위험 요소인 **교착 상태(Deadlock)**와 실행 컨텍스트(Execution Context)의 오용 문제가 도사리고 있다. 앞선 6.1절과 6.2절에서 리스너의 기본 개념과 on_data_available 콜백의 기능적 활용법을 다루었다면, 본 6.3절에서는 리스너가 실제로 시스템 내부, 즉 운영체제와 미들웨어의 경계에서 어떻게 스케줄링되고 실행되는지를 심층적으로 해부한다. 더불어 이 과정에서 발생할 수 있는 치명적인 오류를 방지하기 위해 시스템 엔지니어가 준수해야 할 엄격한 아키텍처 가이드라인과 주요 DDS 구현체별(RTI Connext, OpenDDS, eProsima Fast DDS) 제약 사항을 상세히 분석한다.

1. 리스너 실행 컨텍스트(Listener Execution Context)의 본질적 이해

DDS 애플리케이션 개발자가 가장 빈번하게 범하는 오류는 리스너의 콜백 함수(예: on_data_available, on_subscription_matched 등)가 애플리케이션의 메인 스레드나 개발자가 생성한 별도의 스레드에서 실행된다고 착각하는 것이다. 소스 코드가 애플리케이션 프로젝트 내에 존재하기 때문에 이러한 착시는 자연스럽게 발생한다. 그러나 DDS 표준 명세와 대부분의 구현체 아키텍처에서 리스너는 명백히 **미들웨어(Middleware)가 관리하는 내부 수신 스레드(Internal Receive Thread)**의 컨텍스트에서 실행된다.1 이 사실은 리스너 설계의 모든 제약 사항이 시작되는 지점이다.

1.1 미들웨어 스레드와 업콜(Upcall) 메커니즘의 해부

DDS 미들웨어는 네트워크(UDP, TCP, Shared Memory 등)로부터 유입되는 방대한 패킷을 실시간으로 처리하기 위해 백그라운드에서 하나 이상의 수신 스레드를 구동한다. 데이터가 네트워크 인터페이스 카드(NIC)를 통해 도착하면, 운영체제의 커널 버퍼를 거쳐 미들웨어의 수신 스레드가 이를 픽업(Pick-up)한다. 이 스레드는 RTPS(Real-Time Publish-Subscribe) 프로토콜 헤더를 파싱하고, 서브메시지를 분류하며, 데이터를 역직렬화(Deserialization)하여 엔디안(Endianness)을 맞추고, 적절한 QoS 정책(History, Resource Limits 등)을 적용하여 DataReader의 로컬 캐시에 저장한다.4

리스너의 콜백 함수는 이 복잡한 일련의 과정이 완료된 직후, 동일한 수신 스레드에 의해 호출된다. 이를 시스템 프로그래밍 용어로 ’업콜(Upcall)’이라 칭한다. 즉, 리스너 코드는 애플리케이션 영역에 정의되어 있지만, 이를 실행하기 위해 CPU 자원을 할당받고 스택 프레임을 사용하는 주체는 미들웨어 영역이다. 이것은 마치 트로이 목마와 같다. 미들웨어 스레드가 애플리케이션 코드 내부로 진입하여 코드를 실행하는 것이다.

실행 단계실행 주체 (Thread Context)작업 내용 및 특징
1. 데이터 수신Middleware Internal Thread소켓 읽기 (recv), RTPS 프로토콜 헤더 및 서브메시지 파싱
2. 데이터 처리Middleware Internal Thread데이터 역직렬화(Deserialization), 필터링, QoS 정책 확인, Reader Cache 업데이트
3. 락 획득Middleware Internal Thread데이터 일관성 보장을 위한 내부 뮤텍스(Entity Lock/EA) 획득
4. 알림(Notification)Middleware Internal Threadon_data_available() 등 리스너 콜백 함수 호출 (Upcall)
5. 사용자 로직Middleware Internal Thread사용자가 구현한 콜백 내부 로직 수행 (데이터 접근, 로깅, 재전송 등)
6. 복귀(Return)Middleware Internal Thread미들웨어 내부 루프 복귀, 락 해제, 다음 패킷 처리 대기

위 표에서 알 수 있듯이, 4단계와 5단계에서 수행되는 사용자 정의 로직은 미들웨어의 핵심 통신 처리 과정의 일부로 편입된다. 이는 리스너 내부에서 수행되는 모든 작업이 미들웨어의 통신 처리 과정을 잠시 중단시키고 수행된다는 것을 의미한다.2 만약 리스너 내부에서 과도한 연산을 수행하거나, 파일 I/O와 같은 블로킹(Blocking) 작업을 호출하거나, 스레드를 잠재우는(Sleep) 행위를 한다면, 해당 수신 스레드는 멈추게 된다. 이는 곧 네트워크 패킷 손실, Heartbeat 메시지 처리 지연, 나아가 전체 DDS 도메인의 통신 마비로 이어지는 연쇄적인 장애를 유발한다.

1.2 구현체별 스레딩 모델과 영향 범위

DDS 벤더 구현체와 QoS 설정에 따라 리스너를 실행하는 스레드의 개수와 할당 방식이 달라지며, 이는 리스너 지연 시 시스템에 미치는 영향 범위를 결정한다.

  • 단일 수신 스레드 모델 (Single Receive Thread): 임베디드 시스템용 경량화 설정이나 특정 전송 계층(Transport) 설정에서는 하나의 DomainParticipant 내 모든 DataReader가 단 하나의 수신 스레드를 공유할 수 있다. 이 경우, 특정 DataReader A의 리스너가 지연되면, 전혀 관계없는 DataReader B의 데이터 수신까지 차단되는 치명적인 병목 현상이 발생한다.3 이는 시스템의 격리성(Isolation)을 심각하게 훼손한다.
  • 멀티 스레드 수신 모델 (Multi-threaded Receive Model): 일반적으로는 전송 프로토콜(UDPv4, SHM 등) 당 스레드를 생성하거나, Subscriber마다 별도의 스레드를 할당하는 방식이 사용된다.5 예를 들어, eProsima Fast DDS나 RTI Connext는 전송 포트나 Subscriber 구성에 따라 스레드 풀을 운영한다. 그러나 이 경우에도 동일한 전송 채널이나 Subscriber 그룹(Subscriber 내의 모든 DataReader는 기본적으로 리스너 스레드를 공유할 수 있음) 내에서는 여전히 직렬화된 처리(Serialized Processing)가 이루어진다.

따라서 개발자는 “내 코드가 미들웨어의 심장부에서, 미들웨어의 권한으로 실행되고 있다“는 경각심을 가지고 리스너를 설계해야 한다.

2. 교착 상태(Deadlock) 발생의 해부학

멀티스레드 프로그래밍에서 교착 상태는 두 개 이상의 스레드가 서로 상대방이 점유한 자원(Lock)을 기다리며 무한 대기 상태에 빠지는 현상이다. DDS 리스너 컨텍스트에서 발생하는 교착 상태는 일반적인 애플리케이션 데드락보다 훨씬 복잡하며, 주로 **애플리케이션 레벨의 뮤텍스(App Mutex)**와 **미들웨어 내부의 뮤텍스(Internal Mutex)**가 보이지 않게 얽히면서 발생한다. 이는 코프먼(Coffman)의 4가지 교착 상태 조건 중 ’상호 배제(Mutual Exclusion)’와 ‘환형 대기(Circular Wait)’ 조건이 미들웨어와 애플리케이션 경계에서 충족되기 때문이다.

2.1 시나리오 A: 역순 잠금(Inverse Locking Order) - 가장 흔한 데드락

이 시나리오는 애플리케이션 스레드가 DDS 엔티티(Entity)에 접근하면서 애플리케이션 자체 락(AppLock)을 획득한 상태에서, 동시에 리스너가 호출되면서 발생한다. RTI Connext 및 주요 DDS 벤더들은 이 위험성을 강력하게 경고한다.2

발생 메커니즘:

  1. 애플리케이션 스레드(Main Thread):
  • 어떤 비즈니스 로직(예: GUI 업데이트, 데이터베이스 쓰기)을 수행하기 위해 애플리케이션 레벨의 App_Mutex를 획득한다.
  • 그 상태에서 데이터를 읽거나 상태를 확인하기 위해 DDS API(예: DataReader.read(), Subscriber.get_qos())를 호출한다.
  • DDS API는 내부 데이터 구조의 무결성을 보장하기 위해 미들웨어 내부 락인 Middleware_Entity_Lock (RTI의 경우 EA: Exclusive Area)을 획득하려고 시도한다.
  1. 미들웨어 스레드(Receive Thread):
  • 네트워크에서 데이터를 수신하여 처리하는 과정에서 이미 해당 엔티티를 보호하기 위한 Middleware_Entity_Lock을 획득한 상태이다.
  • 데이터 처리가 끝난 후 사용자에게 알림을 주기 위해 on_data_available() 콜백을 호출한다.
  • 사용자가 작성한 콜백 함수 내부에서 공유 자원(예: 전역 변수, GUI 객체)에 접근하기 위해 App_Mutex를 획득하려고 시도한다.

결과: 환형 대기(Circular Wait) 형성

  • 애플리케이션 스레드: App_Mutex 보유 → Middleware_Entity_Lock 대기.
  • 미들웨어 스레드: Middleware_Entity_Lock 보유 → App_Mutex 대기.
  • -> 영구적인 교착 상태(Deadlock).

이러한 AB-BA 교착 상태는 디버깅이 매우 어렵다. 특히 데이터 수신 빈도가 낮거나 스레드 스케줄링 타이밍이 정확히 겹치지 않으면 테스트 단계에서는 발견되지 않다가, 트래픽이 폭주하는 실제 운영 환경에서 간헐적으로 시스템을 멈추게 만든다.8

2.2 시나리오 B: 리스너 내부에서의 쓰기(Write) 호출과 재진입성(Reentrancy)

많은 개발자가 수신된 데이터를 가공하여 즉시 다른 Topic으로 재전송(Republish)하는 로직을 리스너 내부에 구현하고자 한다(Request-Reply 패턴 등). 그러나 리스너 내부에서 DataWriter.write()를 호출하는 것은 특정 조건 하에서 데드락을 유발할 수 있다.2

원인 분석:

  • 리소스 제한(Resource Limits)에 의한 블로킹: 만약 DataWriterKEEP_ALL QoS와 엄격한 RELIABLE 설정을 사용 중이고, 송신 버퍼(Send Window)가 가득 찬 상태라면 write() 함수는 공간이 생길 때까지 블로킹된다.2 이때 공간이 생기려면 미들웨어 스레드가 수신 측의 ACK(Acknowledgement)를 처리(Clean-up)해주어야 한다. 그러나 ACK를 처리해야 할 미들웨어 스레드는 현재 리스너 함수 안에 갇혀 있다. 즉, write()는 자신이 실행 중인 스레드가 할 일을 끝내기를 기다리는 자기 모순에 빠진다. 이는 전형적인 자원 기아형 데드락이다.
  • 내부 락의 재진입 불가: 일부 DDS 구현체의 경우 DataWriterDataReader가 동일한 Publisher/Subscriber 혹은 DomainParticipant 수준의 상위 락을 공유할 수 있다. 리스너(DataReader 컨텍스트)가 이미 상위 락을 잡고 있는데, 내부에서 호출된 write()가 다시 그 상위 락을 요구할 경우, 해당 뮤텍스가 재진입(Reentrant) 가능하도록 설계되지 않았다면 데드락에 걸린다.8

2.3 시나리오 C: 엔티티 생성/삭제 (Creation/Deletion)

리스너 콜백 내부에서 새로운 DDS 엔티티(다른 DataReader, DataWriter, Topic 등)를 생성하거나 기존 엔티티를 삭제하는 행위는 대부분의 DDS 구현체에서 엄격히 금지되거나 매우 위험한 작업으로 분류된다.7

  • 락 계층 구조 위반: 엔티티를 생성하거나 삭제하려면 상위 팩토리 객체(DomainParticipant 등)의 락을 획득해야 한다. 리스너는 이미 하위 엔티티의 락을 쥐고 있는 상태이므로, 이 상태에서 상위 락을 요청하는 것은 락 획득 순서(Locking Hierarchy)를 위반할 가능성이 매우 높다.
  • RTI Connext의 제약: RTI는 리스너 콜백 중 해당 엔티티가 속한 Exclusive Area (EA)가 잠긴 상태임을 명시한다. 이 상태에서 엔티티 생성/삭제 시도는 RETCODE_ILLEGAL_OPERATION을 반환하거나 데드락을 유발한다.7
  • Fast DDS의 제약: Fast DDS 문서 역시 리스너 멤버 함수 내에서 엔티티를 생성하거나 삭제해서는 안 된다고 경고한다. 이는 미정의 동작(Undefined Behavior)이나 데드락을 유발할 수 있다.10

3. 주요 DDS 벤더별 리스너 제약 사항 비교 분석

DDS 표준은 인터페이스를 정의하지만, 스레딩 모델과 락킹 메커니즘은 벤더별 구현 상세(Implementation Detail)에 해당한다. 따라서 각 구현체가 제시하는 리스너 내 금지 작업을 숙지하는 것은 포팅 가능한 DDS 애플리케이션 작성에 필수적이다.

3.1 RTI Connext DDS (Professional & Micro)

RTI는 **EA(Exclusive Area)**라는 개념을 통해 내부 동시성을 관리한다. 리스너가 호출될 때는 해당 엔티티의 EA에 진입한 상태로 간주된다. 각 PublisherSubscriber는 자신만의 EA를 가지며, 이는 자식 엔티티들과 공유된다.

RTI Connext 리스너 내 금지된 작업 (Restricted Operations) 7:

  1. 엔티티 관리: 어떤 엔티티든 생성(create_xxx), 삭제(delete_xxx), 활성화(enable), QoS 설정(set_qos) 금지.
  2. 교차 호출:
  • DataWriter 리스너 내에서: 다른 Publisher, DataWriter 호출 금지. 모든 Subscriber, DataReader 호출 금지.
  • DataReader 리스너 내에서: 다른 Subscriber, DataReader 호출 금지.
  1. 특정 API: wait_for_acknowledgments() 호출 절대 금지 (데드락 직결).2
  2. 예외: get_key_value, create_data, delete_data 등 일부 로컬 연산은 안전하게 호출 가능하다.13

3.2 eProsima Fast DDS

Fast DDS는 고성능을 위해 내부적으로 복잡한 스레딩 모델을 사용하며, C++11 표준 스레드와 뮤텍스를 적극 활용한다. 리스너 사용 시 다음 사항을 주의해야 한다.5

Fast DDS 리스너 제약 사항:

  1. 엔티티 생성/삭제 금지: 리스너 콜백 내에서 create_datareader, create_datawriter 등을 호출하면 내부 자원 락(Resource Lock) 충돌로 인해 데드락이 발생할 수 있다. 특히 디스커버리 콜백(on_participant_discovery 등) 내에서 이에 대응하는 엔티티를 즉시 생성하는 패턴은 데드락의 주원인으로 지목된다.11
  2. 디스커버리 지연: 디스커버리 콜백 내에서 무거운 작업을 수행하면 전체 디스커버리 프로세스가 지연되어 시스템 초기화 속도가 현저히 느려진다.
  3. 안전성 이슈: 초기 버전이나 특정 설정에서는 리스너 스레드에서 객체를 생성하는 것이 허용되지 않았으며, 이를 위반 시 프로세스가 멈추는(Hang) 이슈가 다수 보고되었다.11 Fast DDS는 이를 피하기 위해 리스너 클래스를 정보 전달 채널로만 사용하고, 실제 객체 생성은 상위 엔티티나 별도 스레드에 위임할 것을 권장한다.

3.3 OpenDDS

OpenDDS는 ACE/TAO 프레임워크 기반의 Reactor 패턴을 사용하며, 리스너는 Reactor 스레드(Service Thread)에서 실행된다.15

OpenDDS 리스너 제약 사항:

  1. Reactor 스레드 차단: 리스너가 블로킹되면 Reactor 스레드가 멈추게 되어, 동일한 Reactor를 공유하는 다른 I/O 처리와 타이머 이벤트가 모두 중단된다.16
  2. Service_Participant 락: Service_Participant 전역 객체의 락이나 DCPSInfoRepo와의 통신이 리스너 내부 작업과 얽히지 않도록 주의해야 한다.
  3. 언어 바인딩 이슈: 파이썬(Python) 바인딩 사용 시, 리스너 콜백(C++ 스레드)이 파이썬 코드를 호출하려면 GIL(Global Interpreter Lock)을 획득해야 한다. 이때 메인 스레드가 GIL을 잡고 DDS 호출을 하여 C++ 락을 기다리는 상황이 발생하면, 전형적인 교착 상태가 발생한다.8

4. 블로킹(Blocking)과 프로토콜 기아(Protocol Starvation) 현상

교착 상태만큼이나 위험한 것이 리스너의 ’블로킹’이다. 리스너가 데드락에 걸리지 않더라도, 긴 시간 동안 실행되거나(Heavy Processing), 파일 I/O 대기, sleep() 호출 등으로 인해 스레드를 점유하고 있으면 프로토콜 기아(Protocol Starvation) 현상이 발생한다.2

4.1 Heartbeat 및 ACKNACK 처리 불가와 그 여파

DDS의 신뢰성(Reliability) 프로토콜은 주기적인 Heartbeat 메시지(DataWriter가 보냄)와 이에 대한 ACKNACK 메시지(DataReader가 보냄)의 교환에 전적으로 의존한다.18

  • 문제 상황: DataReader의 리스너가 블로킹되어 있으면, 해당 수신 스레드는 멈춰 있는 상태이다. 따라서 DataWriter가 보내는 Heartbeat 메시지를 수신 스레드가 처리(Process)할 수 없다. Heartbeat를 처리하지 못하면 DataReader는 자신이 어떤 데이터를 놓쳤는지, 혹은 잘 받았는지를 알리는 ACKNACK 메시지를 생성하여 보낼 수 없다.
  • 연쇄적 장애:
  1. Writer 측 송신 중단: DataWriter는 DataReader로부터 ACKNACK(긍정/부정 응답)을 받지 못하므로, 해당 Reader를 ‘응답 없음’ 상태로 간주한다.
  2. Send Window 포화: DataWriter의 송신 버퍼(Send Window)는 전송했지만 확인받지 못한 샘플들로 가득 차게 된다.
  3. Writer 블로킹: 결국 DataWriter 측에서도 write() 함수가 블로킹되거나, KEEP_LAST 정책에 의해 오래된 데이터가 덮어씌워지면서 데이터 손실이 발생한다.19
  4. 비활성(Inactive) 처리: 일정 시간(max_heartbeat_retries) 이상 응답이 없으면 DataWriter는 해당 Reader와의 연결을 끊어버릴 수 있다.20

4.2 데이터 손실 및 네트워크 버퍼 오버플로우

운영체제 커널 수준에서도 심각한 문제가 발생한다. UDP 버퍼는 유한하다. 리스너가 멈춰 있으면 미들웨어는 소켓에서 데이터를 퍼올리지(Recv) 못한다.

  • 소켓 버퍼 오버플로우: 커널의 수신 소켓 버퍼(Receive Socket Buffer)가 가득 차면, 운영체제는 이후 네트워크 카드로 도착하는 패킷을 무차별적으로 드롭(Drop)한다.2 이는 DDS 수준의 신뢰성 프로토콜이 개입하기도 전에 데이터가 증발하는 것을 의미한다.
  • 재전송 폭풍(Retransmission Storm): 패킷이 드롭되면 DDS는 재전송을 시도하게 된다. 그러나 리스너는 여전히 블로킹 상태이므로 재전송된 패킷도 처리되지 못하고 다시 드롭된다. 이 과정에서 불필요한 재전송 트래픽이 네트워크 대역폭을 잠식하여 상황을 더욱 악화시킨다.19

따라서, **“리스너는 절대 블로킹되어서는 안 된다(Never block in a listener callback)”**는 DDS 프로그래밍의 제1원칙이며, 이를 어길 시 시스템 전체의 안정성이 붕괴된다.3

5. 안전한 리스너 설계를 위한 아키텍처 패턴

교착 상태와 블로킹 문제를 원천적으로 해결하고 시스템의 견고함을 보장하기 위해, 다음의 3가지 아키텍처 패턴을 상황에 맞게 적용해야 한다.

5.1 패턴 1: 큐 오프로딩(Queue Offloading) - 권장 패턴

가장 안전하고 널리 사용되는 패턴은 리스너가 데이터 처리를 직접 수행하지 않고, 데이터를 애플리케이션 스레드로 넘겨주는 역할(Dispatcher)만 수행하는 것이다.22 이 패턴은 생산자-소비자(Producer-Consumer) 모델을 DDS 수신부에 적용한 것이다.

구현 단계:

  1. 리스너 역할 (생산자): on_data_available이 호출되면 take()를 통해 데이터를 미들웨어 캐시에서 가져온다.
  2. 데이터 이동: 가져온 데이터를 **스레드 안전한 큐(Thread-Safe Queue)**에 넣는다(Push). 이때 큐 삽입 연산은 Non-blocking이거나 타임아웃이 매우 짧아야 한다. 데이터 복사 비용을 줄이기 위해 스마트 포인터나 이동 시멘틱(Move Semantics)을 활용하는 것이 좋다.
  3. 애플리케이션 역할 (소비자): 별도의 워커 스레드(Worker Thread)가 큐에서 데이터를 꺼내(Pop) 복잡한 비즈니스 로직(DB 저장, 대용량 연산, GUI 업데이트 등)을 수행한다.

이 패턴을 사용하면 미들웨어 스레드는 데이터를 큐에 넣는 즉시 리턴하여 다음 패킷 처리에 복귀할 수 있으며, 애플리케이션 로직에서 어떤 락을 잡든 미들웨어 락과 충돌할 일이 없다.

C++

// 개념적 구현 예시 (C++)
// 리스너 구현 (미들웨어 스레드 컨텍스트)
void MyListener::on_data_available(DataReader* reader) {
SampleSeq data;
InfoSeq info;
// 1. 데이터 가져오기 (Zero-copy)
reader->take(data, info,...);

// 2. 유효한 데이터만 필터링하여 큐에 삽입
for (int i=0; i < data.length(); ++i) {
if (info[i].valid_data) {
// 주의: 여기서 복잡한 로직 수행 금지. 단순 복사/이동만 수행.
thread_safe_queue.push(data[i]);
}
}

// 3. Loan 반환
reader->return_loan(data, info);
}

// 워커 스레드 구현 (애플리케이션 스레드 컨텍스트)
void WorkerThread::run() {
while(running) {
// 4. 데이터 소비 (여기서는 블로킹되어도 무방함)
auto sample = thread_safe_queue.pop();
process_business_logic(sample); // 락을 걸든, 파일에 쓰든 자유
}
}

5.2 패턴 2: WaitSet 활용 (Pull 방식)

리스너(Push 방식)가 가지는 ‘원치 않는 스레드 컨텍스트’ 문제를 아예 배제하기 위해, DDS 표준이 제공하는 WaitSet을 사용하는 방법이다.2

  • 동작 방식: 애플리케이션 스레드가 WaitSet 객체를 생성하고 StatusCondition을 부착한 뒤 wait()를 호출하여 대기한다. 데이터가 도착하면 wait()가 반환(Unblock)되고, 애플리케이션 스레드가 깨어나 직접 take()를 호출한다.
  • 장점:
  • 데이터 처리가 전적으로 애플리케이션 스레드 컨텍스트에서 이루어진다.
  • 미들웨어 락과 애플리케이션 락의 충돌(AB-BA Deadlock) 가능성이 구조적으로 제거된다.
  • 플로우 제어(Flow Control)가 자연스럽다. 애플리케이션이 바쁘면 wait()를 호출하지 않거나 늦게 호출함으로써 데이터 처리 속도를 스스로 조절할 수 있다.
  • 단점: 별도의 스레드를 생성 및 관리해야 하며, 이벤트 루프를 직접 구현해야 하므로 리스너 방식보다 코드가 다소 복잡해질 수 있다. 또한 wait()에서 깨어나는 과정에서 컨텍스트 스위칭 비용이 발생하여 리스너보다 미세하게 지연 시간이 증가할 수 있다.23

[표 6-1] 리스너(Listener)와 웨이트셋(WaitSet) 비교 분석

특성리스너 (Listener)웨이트셋 (WaitSet)
실행 컨텍스트미들웨어 내부 스레드사용자 애플리케이션 스레드
데드락 위험매우 높음 (설계 시 주의 필요)매우 낮음
지연 시간(Latency)매우 낮음 (즉시 호출)약간 높음 (스레드 깨움/전환 비용)
처리량(Throughput)블로킹 시 급격히 저하배치(Batch) 처리에 유리하여 고부하 시 우수
사용 용도단순 로깅, 상태 모니터링, 초저지연 처리복잡한 데이터 처리, GUI 연동, 안전성 중요 시스템
권장 여부간단하고 빠른 작업에만 권장일반적인 데이터 처리 및 안전성 중시 시스템에 권장

5.3 패턴 3: 상태 플래그와 폴링 (Hybrid)

GUI 애플리케이션(Qt, MFC 등)이나 게임 엔진과 같이 메인 루프(Main Loop)가 이미 존재하는 경우, 리스너는 단순히 “데이터가 도착했다“는 플래그(Atomic Boolean 등)만 true로 설정하고 즉시 리턴한다. 메인 루프는 주기적으로 이 플래그를 확인하여 true일 경우 데이터를 take()한다.

  • 장점: 큐나 멀티스레드 동기화의 복잡성 없이도 안전성을 확보할 수 있다.
  • 단점: 폴링 주기에 따라 지연(Jitter)이 발생하며, 실시간성이 다소 떨어진다.

6. 고급 주제: 리스너와 실시간성(Real-Time) 및 우선순위 역전

실시간 운영체제(RTOS)나 Linux의 PREEMPT_RT 커널을 사용하는 미션 크리티컬 시스템에서 리스너의 우선순위 관리는 시스템의 결정성(Determinism)을 좌우한다.

  • 우선순위 역전(Priority Inversion): DDS 미들웨어 수신 스레드는 패킷 유실을 막기 위해 통상적으로 높은 우선순위(High Priority)를 가진다. 만약 리스너 내부에서 낮은 우선순위(Low Priority) 스레드가 점유하고 있는 일반 뮤텍스를 획득하려고 대기한다면 어떻게 될까? 높은 우선순위의 미들웨어 스레드가 낮은 우선순위 작업이 끝날 때까지 기다려야 하는 상황이 발생한다. 이를 ’우선순위 역전’이라 하며, 이는 실시간 시스템의 엄격한 데드라인(Deadline) 위반을 초래한다.6
  • 우선순위 상속(Priority Inheritance): 이를 방지하기 위해 뮤텍스에 우선순위 상속 프로토콜(Priority Inheritance Protocol)을 적용해야 한다. 그러나 가장 근본적인 해결책은 애초에 리스너 내에서 공유 자원 접근을 최소화하여 락 대기 상황을 만들지 않는 것이다.

7. 요약 및 결론: 안전한 리스너 구현을 위한 체크리스트

6.3절의 논의를 종합하여, 엔지니어가 리스너를 작성할 때 반드시 확인해야 할 6가지 핵심 원칙을 제시한다. 이 체크리스트는 코드 리뷰 시 필수 점검 항목으로 활용되어야 한다.

  1. Context Awareness: “이 코드는 내가 만든 스레드가 아니라, 미들웨어 스레드가 실행한다“는 사실을 주석 최상단에 명시하고 항상 상기하라.
  2. No Blocking: 리스너 내에서 sleep(), 소켓 통신, 대용량 파일 쓰기, 과도한 연산 등 블로킹 작업을 절대 수행하지 말라.
  3. No Application Locks: 가능한 한 리스너 내에서 애플리케이션 뮤텍스를 획득하지 말라. 불가피할 경우 try_lock()을 사용하거나 락 프리(Lock-free) 자료구조를 사용하라.
  4. No Restricted Calls: 리스너 내에서 create_entity, delete_entity, set_qos 등의 관리형 API를 호출하지 말라. 이는 각 벤더별 금지 사항 1순위다.
  5. Offload Quickly: 데이터 처리 로직이 1ms 이상 소요된다면 즉시 큐나 버퍼로 데이터를 복사하고 리턴하는 오프로딩 패턴을 적용하라.
  6. Consider WaitSet: 만약 리스너의 제약 사항을 준수하기 어렵거나 로직이 복잡하다면, 주저 없이 WaitSet 패턴으로 설계를 변경하라. 이것이 장기적인 유지보수와 안정성 측면에서 훨씬 유리하다.

리스너는 강력하지만 양날의 검이다. 올바른 실행 컨텍스트의 이해와 교착 상태 방지 가이드를 철저히 준수함으로써, 개발자는 DDS가 제공하는 마이크로초 단위의 고성능을 유지하면서도, 어떤 부하 상황에서도 멈추지 않는 견고한 분산 시스템을 구축할 수 있다. 이어지는 제7장에서는 이러한 데이터 읽기 모델을 넘어, 대규모 데이터 전송 시 메모리 복사 비용을 제로에 가깝게 줄이는 ‘Zero-Copy’ 기술과 고급 메모리 관리 기법에 대해 다룰 것이다.

8. 참고 자료

  1. Data Distribution Service (DDS) - Object Management Group (OMG), https://www.omg.org/spec/DDS/1.4/PDF
  2. Package: DDS.Listener - RTI Community, https://community.rti.com/static/documentation/connext-dds/current/doc/api/connext_dds/api_ada/dds-listener.ads.html
  3. Never Block in a Listener Callback - RTI Community, https://community.rti.com/best-practices/never-block-listener-callback
  4. Bounding the Data-Delivery Latency of DDS Messages in Real-Time …, https://drops.dagstuhl.de/storage/00lipics/lipics-vol262-ecrts2023/LIPIcs.ECRTS.2023.9/LIPIcs.ECRTS.2023.9.pdf
    1. Library Overview - 3.4.1 - eProsima Fast DDS, https://fast-dds.docs.eprosima.com/en/3.x/fastdds/library_overview/library_overview.html
  5. End-to-End Latency Optimization of Thread Chains Under the DDS …, https://retis.santannapisa.it/~d.casini/papers/2024/DATE2024/Sciangula2024.pdf
  6. Restricted Operations in Listener Callbacks - RTI Community, https://community.rti.com/static/documentation/connext-dds/current/doc/manuals/connext_dds_professional/users_manual/users_manual/Restricted_Operations_in_Listener_Callba.htm
  7. Thread deadlock when attempting to transmit data from Listener …, https://github.com/eclipse-cyclonedds/cyclonedds-python/issues/7
  8. Deadlock: Who Owns the Lock? - Joseph Mate, https://josephmate.github.io/2020-02-24-deadlock-who-owns-the-lock/
  9. 3.1.1. Entity — Fast DDS 2.6.10 documentation, https://fast-dds.docs.eprosima.com/en/v2.6.10/fastdds/dds_layer/core/entity/entity.html
  10. DeathLock · Issue #3058 · eProsima/Fast-DDS - GitHub, https://github.com/eProsima/Fast-DDS/issues/3058
  11. Issue #3138 · eProsima/Fast-DDS - GitHub, https://github.com/eProsima/Fast-DDS/issues/3138
  12. Package: DDS.DataWriterListener - RTI Community, https://community.rti.com/static/documentation/connext-dds/7.3.0/doc/api/connext_dds/api_ada/dds-datawriterlistener.ads.html
  13. Deadlock in v2.6.2 · Issue #2961 · eProsima/Fast-DDS - GitHub, https://github.com/eProsima/Fast-DDS/issues/2961
  14. Introduction to OpenDDS - OpenDDS 3.29.1, https://opendds.readthedocs.io/en/dds-3.29.1/devguide/introduction.html
  15. OpenDDS Developer’s Guide - Huihoo, https://docs.huihoo.com/opendds/OpenDDS-2.0.1-Developer-Guide.pdf
  16. OpenDDS/docs/history/NEWS-0.md at master - GitHub, https://github.com/objectcomputing/OpenDDS/blob/master/docs/history/NEWS-0.md
  17. Controlling Heartbeats and Retries with DataWriterProtocol QosPolicy, https://community.rti.com/static/documentation/connext-dds/current/doc/manuals/connext_dds_professional/users_manual/users_manual/Controlling_Heartbeats_and_Retries.htm
  18. The Top 10 Reasons for Dropped DDS Messages - RTI, https://www.rti.com/blog/top-10-reasons-for-dropped-dds-messages
  19. RTI Connext Micro C++ API - RTI Community, https://community.rti.com/static/documentation/connext-micro/current/doc/api_cpp/html/structDDS__RtpsReliableWriterProtocol__t.html
  20. 15.5. Large Data Rates - 3.4.1 - eProsima Fast DDS, https://fast-dds.docs.eprosima.com/en/3.x/fastdds/use_cases/large_data/large_data.html
  21. Thread Safe Template Message Queue in C++17 - GitHub Gist, https://gist.github.com/CaglayanDokme/2fd4278969cf9683c5e5ad0ca5534020
  22. Use WaitSets, Except When You Need Extreme Latency, https://community.rti.com/best-practices/use-waitsets-except-when-you-need-extreme-latency
  23. core — Eclipse Cyclone DDS Python documentation, https://cyclonedds.io/docs/cyclonedds-python/0.10.1/cyclonedds.core.html